Python的异常机制
目录
前言
主流编程语言主要有两种处理异常的模型,一种是错误返回值,比如:Go 和 C,都是通过返回值来判断是否发生异常情况。另外一种,是异常模型,比如:Java,Python,C#,C++等。
本文主要讲述 Python 的异常机制。
SyntaxError
有一种异常比较特殊,它不是发生在运行时(RunTime),而是在 Python 解释器解析代码时可能会爆出的异常。
例如:
>>> print())
File "<stdin>", line 1
print())
^
SyntaxError: unmatched ')'
>>> print(')
File "<stdin>", line 1
print(')
^
SyntaxError: unterminated string literal (detected at line 1)
>>> print(;)
File "<stdin>", line 1
print(;)
^
SyntaxError: invalid syntax
>>> try:
... print('ok')
...
File "<stdin>", line 3
^
SyntaxError: expected 'except' or 'finally' block
Python 是一种解释型语言,解释器在执行代码之前会先对代码进行语法检查,如果代码语法违法了 Python 语言的规则,就会抛出 SyntaxError。
Exception
如果代码语法没问题,程序运行起来后,也可能会存在各种运行时的错误,一般把这种错误都称为异常(Exception)。
以最经典的除零异常为例:
print(1/0)
异常信息如下:
Traceback (most recent call last):
File "C:\Users\ABC\Desktop\main.py", line 1, in <module>
print(1/0)
~^~
ZeroDivisionError: division by zero
错误信息的最后一行说明程序遇到了什么类型的错误。异常有不同的类型,比如这里的 ZeroDivisionError。
可以简单的通过类型判断错误大致上属于类别,对于所有内置异常都会打印出异常类型。
异常类型后面的内容则是更加详细的描述信息。
错误信息的开头使用堆栈回溯的形式展示发生异常时的上下文环境,比如存在多个函数嵌套的异常:
def a():
b()
def b():
c()
def c():
print(1/0)
if __name__ == '__main__':
a()
异常信息如下:
Traceback (most recent call last):
File "C:\Users\ABC\Desktop\main.py", line 11, in <module>
a()
File "C:\Users\ABC\Desktop\main.py", line 2, in a
b()
File "C:\Users\ABC\Desktop\main.py", line 5, in b
c()
File "C:\Users\ABC\Desktop\main.py", line 8, in c
print(1/0)
~^~
ZeroDivisionError: division by zero
抛出异常
上面提到的是程序自动抛出异常,但我们有时候碰到一些特殊情况,想要手动触发异常,那么可以使用 raise 关键字。
假设现在需要用户输入一个数字,需要比 10 大,否则抛出一个异常
num_str = input('Enter a number: ')
num = int(num_str)
if num > 10:
print('ok')
else:
raise Exception(f'number: {num} <= 10')
输入一个小于或等于 10 的数字,抛出异常信息:
Enter a number: 1
Traceback (most recent call last):
File "C:\Users\ABC\Desktop\main.py", line 8, in <module>
raise Exception(f'number: {num} <= 10')
Exception: number: 1 <= 10
捕获以及处理异常
如果对于程序的异常没有做任何处理,那么,Python 会自动调用 sys.exit() 来终止程序的执行。
但很多时候,我们碰到异常情况也许可以做一些补救或者记录性的事情,这时候就需要在程序的某个地方捕获异常。
还是以用户输入一个数字为例子:
num_str = input('Enter a number: ')
num = int(num_str)
print(f'user input number: {num}')
如果用户输入错误,比如输入了字符串,int 函数会抛出异常,而我们没有任何处理,程序打印完异常信息后,直接退出;
Enter a number: f1
Traceback (most recent call last):
File "C:\Users\ABC\Desktop\main.py", line 3, in <module>
num = int(num_str)
^^^^^^^^^^^^
ValueError: invalid literal for int() with base 10: 'f1'
这时候可以使用 try-except 捕获和处理异常:
num_str = input('Enter a number: ')
try:
num = int(num_str)
except:
print(f'number incorrect ({num_str=}), use default number.')
num = -1
print(f'user input number: {num}')
这里使用 try 捕获了 int 可能由于用户输入错误而抛出的异常,并且在 except 中进行了处理(打印一条信息并使用默认值),由于我们处理了异常,所以程序并不会直接退出。
try 语句的目的在于给一组语句指定异常处理器和清理性质的代码。
except 子句跟随 try 语句出现,主要作用是匹配 try 块中代码可能出现的异常类型,如果 except 后面没有任何表达式,那么该 except 子句将匹配任何类型的异常。
在 except 后面不加任何异常类型,初看感觉很简洁,写起来也方便,但在生产中是一种比较偷懒的写法。
假设 try 块中有多行代码,都可能引发各种类型的异常:
import requests
try:
response = requests.get('https://example.com', timeout=5)
f = open('./content.txt', 'w', encoding='utf-8')
f.write(response.text)
except:
print('Some errors happened, but I do not know...')
代码逻辑很简单,通过网络请求一个站点,然后把 HTTP 响应的 text body 写到本地的一个文件中,这个过程可能会有各种各样的异常,服务器请求错误(404,500),网络超时,文件无法打开,文件写入错误等等。
如果 except 简单忽略掉后面的异常类型,那么它会匹配到所有异常类型,最终我们在 except 的处理代码中,只能知道出错了,但是不知道具体发生了什么错误。
最常见的一个请求错误是读取超时,国内访问外网的时候经常发生,除此之外 HTTP response code 不为 200 的情况也时有发生,如果我们需要将这些异常分开捕获,那么需要在每一个 except 子句中,指定对应的异常类型:
import requests
try:
response = requests.get("http://xxx123example.com", timeout=10)
response.raise_for_status()
print(response.text)
except requests.exceptions.ReadTimeout as e:
print(f'Read timeout error: {e}')
except requests.exceptions.ConnectionError as e:
print(f'Connection error: {e}')
except requests.exceptions.HTTPError as e:
print(f'HTTP error: {e}')
print('task end...')
不同的异常类型被不同的 except 子句捕获后处理。
捕获的顺序
try-except 语句执行的顺序如下:
- 执行 try 块中的多行代码。
- 如果 try 块中多行代码没有抛出任何异常,则跳过 except 子句,try-except 执行完毕。
- 如果在 try 块中某一行代码触发了异常,那么跳过 try 块中剩余的其他代码,进入到 except 匹配。
- except 匹配自上到下,如果异常的类型和某一个 except 子句后指定的异常相匹配,则进入到 except 块执行代码,try-except 执行完毕。
- 如果 except 没有匹配到,则该异常会被传递到外层,如果一直没有找到合适的处理器,那么它就是一个未处理异常,程序将停止运行并输出一条错误消息。
try-except 中可以存在多个 except 子句,但最多只有一个 except 会被执行。
从上到下的捕获顺序规则:
- try 块出现异常的时候,Python 会从上到下按顺序,依次检查每一个 except,尝试匹配。
- 第一个匹配成功的 except 会被执行,其余忽略。
- 存在多个 except 时,要保证子类异常在前,父类异常在后,否则会导致,父类先捕获异常而子类永远不会执行。
最后一个规则是由于 exception 存在继承关系,Python 内部异常继承关系如下:
BaseException
├── BaseExceptionGroup
├── GeneratorExit
├── KeyboardInterrupt
├── SystemExit
└── Exception
├── ArithmeticError
│ ├── FloatingPointError
│ ├── OverflowError
│ └── ZeroDivisionError
├── AssertionError
├── AttributeError
├── BufferError
├── EOFError
├── ExceptionGroup [BaseExceptionGroup]
├── ImportError
│ └── ModuleNotFoundError
├── LookupError
│ ├── IndexError
│ └── KeyError
├── MemoryError
├── NameError
│ └── UnboundLocalError
├── OSError
│ ├── BlockingIOError
│ ├── ChildProcessError
│ ├── ConnectionError
│ │ ├── BrokenPipeError
│ │ ├── ConnectionAbortedError
│ │ ├── ConnectionRefusedError
│ │ └── ConnectionResetError
│ ├── FileExistsError
│ ├── FileNotFoundError
│ ├── InterruptedError
│ ├── IsADirectoryError
│ ├── NotADirectoryError
│ ├── PermissionError
│ ├── ProcessLookupError
│ └── TimeoutError
├── ReferenceError
├── RuntimeError
│ ├── NotImplementedError
│ ├── PythonFinalizationError
│ └── RecursionError
├── StopAsyncIteration
├── StopIteration
├── SyntaxError
│ └── IndentationError
│ └── TabError
├── SystemError
├── TypeError
├── ValueError
│ └── UnicodeError
│ ├── UnicodeDecodeError
│ ├── UnicodeEncodeError
│ └── UnicodeTranslateError
└── Warning
├── BytesWarning
├── DeprecationWarning
├── EncodingWarning
├── FutureWarning
├── ImportWarning
├── PendingDeprecationWarning
├── ResourceWarning
├── RuntimeWarning
├── SyntaxWarning
├── UnicodeWarning
└── UserWarning
考虑一下程序:
import requests
try:
response = requests.get("http://123example.com", timeout=1)
response.raise_for_status()
print(response.text)
except IOError as e:
print(f'io error: {e}')
except requests.exceptions.ReadTimeout as e:
print(f'Read timeout error: {e}')
except requests.exceptions.ConnectionError as e:
print(f'Connection error: {e}')
except requests.exceptions.HTTPError as e:
print(f'HTTP error: {e}')
print('task end...')
由于把 IOError 放在了最上面,而下面所有异常都直接或者间接继承自 IOError,导致发生 ReadTimeout 或者 HTTPError 都会直接被 except IOError 这里直接捕获,后面的 except 就没有了意义。
所以一般建议是:当存在多个 except 块时,从具体的子类再到宽泛的父类异常。
else
如果控制流离开 try 子句体时没有引发异常,并且 try 中没有执行 return, continue 或 break 语句,可选的 else 子句将被执行。
else 语句中的异常不会由之前的 except 子句处理。
finally
finally 子句的目的在于资源清理,无论 try 中有没有发生异常,finally 都会执行,而且和 else 子句不同的一点是,如果 try 中有 return,else 不会执行,而 finally 子句会执行。
def foo():
try:
a = 1
print('try')
return a
except:
pass
else:
print('else')
return a * 2
finally:
print('finally')
return a * 3
res = foo()
print(res)
上面代码的结果是:
try
finally
3
由于 try 中有 return,else 子句不可达,但是 finally 子句一定会执行,并且最后的返回值是 3,try 的 return a 被 finally 中的 return a * 3 覆盖了。
finally 最有用的地方在于处理文件句柄,磁盘、网络IO的资源清理和关闭,如果这些代码不放在 finally 中,那么一旦碰到未处理的异常,这些清理性质的代码可能永远也不会执行,这会造成句柄,连接,内存的泄露。
异常链
理想情况下,try 中发生异常,except 去处理异常,程序走到后续的流程。
但现实是,except 处理异常的代码也可能产生新的异常!
这里就涉及到了异常链的概念,它指的是一个新的未处理异常发生在了 except 内部,Python 会将已被处理的异常信息附加在它上面。
def a():
print("a")
raise Exception('oops...')
def b():
try:
a()
except Exception as e:
print("b")
raise ValueError('value err b') from e
b()
输出结果:
a
b
Traceback (most recent call last):
File "C:\Project\Study\study-python-exception\main.py", line 11, in b
a()
File "C:\Project\Study\study-python-exception\main.py", line 7, in a
raise Exception('oops...')
Exception: oops...
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "C:\Project\Study\study-python-exception\main.py", line 16, in <module>
b()
File "C:\Project\Study\study-python-exception\main.py", line 14, in b
raise ValueError('value err b')
ValueError: value err b
ValueError 是 b 函数在处理 a 函数抛出的异常时,新产生的异常,如果要指明 ValueError 就是由 Exception 引发的,则可以在 raise 后面添加 from:
raise ValueError('value err b') from e
输出如下:
a
b
Traceback (most recent call last):
File "C:\Project\Study\study-python-exception\main.py", line 11, in b
a()
File "C:\Project\Study\study-python-exception\main.py", line 7, in a
raise Exception('oops...')
Exception: oops...
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "C:\Project\Study\study-python-exception\main.py", line 16, in <module>
b()
File "C:\Project\Study\study-python-exception\main.py", line 14, in b
raise ValueError('value err b') from e
ValueError: value err b
参考
- https://docs.python.org/zh-cn/3.13/library/exceptions.html#bltin-exceptions
- https://docs.python.org/zh-cn/3.13/tutorial/errors.html
- https://realpython.com/python-exceptions/
- https://docs.python.org/zh-cn/3.13/reference/executionmodel.html#exceptions
- https://docs.python.org/zh-cn/3.13/reference/compound_stmts.html#try